package cz.cuni.mff.dpp.configuration;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Utility class for configuration and validation of configurable objects.
 * <p>
 * A configurable object is an object that exposes configurable properties. A
 * configurable property is a simple field annotated with the {@link Property}
 * annotation. The values of the properties (i.e. fields) are set using
 * introspection. There are two ways to configure the value of a configurable
 * field:
 * <p>
 * <ol>
 * <li>If the field type is class that provides either a string constructor
 * (i.e. a constructor with a single {@code String} parameter) or a static
 * factory method {@code valueOf (String)} returning an instance of the class,
 * then the value of the field is set to an instance of the field type created
 * from a string representation of the value.</li>
 * <li>If creating instances of the field type is more complex, the field value
 * can be set through a method annotated with the {@link Setter} annotation.
 * Such a method must have a setter signature, i.e. it must return {@code void}
 * and accept a single {@code String} parameter.</li>
 * </ol>
 * <p>
 * Configurable properties of an object can be set using the
 * {@link #set(Object, String, String) set()} method, while validation of object
 * properties can be achieved using the {@link #check(Object) check()} method.
 * The validation ensures that no field representing a mandatory configurable
 * property contains {@code null}.
 */
public class Configurator {

	protected static Logger innerLog = Logger.getLogger(Configurator.class
			.getName());

	/**
	 * Don't use will be removed in future versions
	 */
	@Deprecated
	public static Logger log = Logger.getAnonymousLogger();

	/* ***********************************************************************
	 * ANNOTATIONS
	 * **********************************************************************
	 */

	@Documented
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface Property {

		/**
		 * Returns the name of the configuration property held in the annotated
		 * field.
		 * 
		 * @return name of the property held in the annotated field, or empty
		 *         string indicating that the name is identical to the field
		 *         name
		 */
		String name() default "";

		/**
		 * Determines whether the property is required or not.
		 * 
		 * @return {@code true} if the property must have a non-null value,
		 *         {@code false} otherwise
		 */
		boolean required() default true;

		/**
		 * Returns the string representation of the default value of the
		 * annotated field. Empty string indicates that the default value has
		 * not been set.
		 * 
		 * @return string representation of the default value
		 */
		String defaultValue() default "";

		/**
		 * Returns an optional description of the property held in the annotated
		 * field.
		 * 
		 * @return name
		 */
		String description() default "";
	}

	@Documented
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.METHOD)
	public @interface Setter {

		/**
		 * Returns the name of the property configured by the annotated method.
		 * 
		 * @return name of the property configured by the annotated method, or
		 *         empty string indicating that the name corresponds to method
		 *         name without the "set" prefix with the first letter in lower
		 *         case
		 */
		String name() default "";

	}

	/* ***********************************************************************
	 * PUBLIC METHODS
	 * **********************************************************************
	 */

	/**
	 * Sets the given property to the given value in the given configurable
	 * object.
	 * 
	 * @param target
	 *            target configurable object to set the property on
	 * @param name
	 *            property name
	 * @param value
	 *            property value
	 * @throws ConfigurationException
	 *             if the value of the given property cannot be set on the given
	 *             object
	 */
	public static void set(Object target, String name, String value) {
		//
		// First try to obtain a method based setter, which is necessary
		// for more complex properties. If that fails, try to obtain a
		// field based setter, assuming the property is simple enough to
		// convert to object instance. If that also fails, log a warning.
		//
		PropertySetter setter = makeMethodSetter(target, name);
		if (setter == null) {
			setter = makeFieldPropertySetter(name, target);
			if (setter == null) {
				innerLog.log(Level.WARNING,
						"Unable to find configuration method for property %s",
						name);
				return;
			}
		}

		// seter is now not null
		setter.setValue(value);

	}

	/**
	 * Checks if all configurable fields in the passed object are not null.
	 * 
	 * @param target
	 *            object with configurable fields
	 */
	public static void check(Object target) {
		//
		// Find all configurable fields and make sure that all mandatory
		// fields have a non-null value. If any configurable field with
		// a null value is found and has a default value specified, an
		// instance of the value is created and the field is initialized
		// to the default value.
		//
		// If any mandatory field remains uninitialized, throw an exception.
		//

		try {
			for (Field field : new AllDeclaredFieldsIterable(target.getClass())) {
				//
				// Skip fields without the @Property annotation or fields
				// with non-null value.
				//
				Property property = field.getAnnotation(Property.class);
				if (property != null) {
					Object value = getFieldValue(field, target);
					if (value == null) {

						//
						// Set default value for null fields.
						//
						String name = getFieldName(field, property);
						String defaultValue = getDefaultValue(property);
						if (defaultValue != null) {
							innerLog.log(
									Level.FINE,
									"setting field property %s to default value %s",
									new String[]{name, defaultValue});
							configureFieldProperty(name, target, defaultValue,
									field);
						} else if (property.required()) {
							throw new ConfigurationException(
									"Required property '%s' is not configured",
									name);
						} else {
							innerLog.log(
									Level.FINE,
									"field property %s not set, default value null and not required",
									new String[]{name});
						}
					}
				}

			}

		} catch (ConfigurationException ce) {
			// propagate without wrapping
			throw ce;

		} catch (Exception e) {
			new ConfigurationException(e,
					"Unable to verify object property configuration!");
		}
	}

	/* ***********************************************************************
	 * PACKAGE METHODS
	 * **********************************************************************
	 */

	/* ***********************************************************************
	 * Field-based property setter
	 * **********************************************************************
	 */

	/**
	 * Returns a field based {@link PropertySetter} bound to the given object
	 * and property name. When setting the property value, the returned
	 * {@link PropertySetter} will modify the value of an object field annotated
	 * by the {@code @Property} annotation with matching name.
	 * 
	 * @param name
	 *            name of the property to set
	 * @param target
	 *            target object on which to set the property
	 * 
	 * @return {@link PropertySetter} which allows to configure the property on
	 *         the given object, or {@code null} if the target object has no
	 *         field with matching annotation
	 */
	static PropertySetter makeFieldPropertySetter(final String name,
			final Object target) {
		//
		// Find a configurable field for the given property and create a
		// PropertySetter for the property.
		//
		// Specifically, enumerate all fields in the given object's class
		// hierarchy and find the first field annotated with the
		// @Property annotation matching the given property field.
		//
		for (final Field field : new AllDeclaredFieldsIterable(
				target.getClass())) {
			Property property = field.getAnnotation(Property.class);

			if (property != null) {
				String propertyName = getFieldName(field, property);

				if (name.equals(propertyName)) {
					return new FieldPropertySetter(target, field, propertyName);
				}

			}
		}

		//
		// No match found.
		//
		return null;
	}

	/**
	 * Sets the given field of the given object to the given value. The value is
	 * converted from string representation to an instance of the field type.
	 * 
	 * @param name
	 *            name of the property being configured
	 * @param target
	 *            target object on which to set the field value
	 * @param valueString
	 *            string value of the property being configured
	 * @param field
	 *            the field to set to the given value
	 * 
	 * @throws ConfigurationException
	 *             if the string representation of the value could not be
	 *             converted to an instance of the field type or if setting the
	 *             field failed
	 */
	static void configureFieldProperty(String name, Object target,
			String valueString, Field field) {
		//
		// Create an instance of the property value and set the field
		// value on the target object.
		//
		Object value = makeValueInstance(field, valueString);
		if (value == null) {
			throw new ConfigurationException(
					"property %s: could not create %s instance for %s", name,
					field.getType().getName(), valueString);
		}

		try {
			setFieldValue(field, target, value);
		} catch (Exception e) {
			new ConfigurationException(e,
					"Unable to configure field %s with property %s=%s",
					field.getName(), name, valueString);
		}
	}

	/**
	 * Creates an object instance from the string representation of a property
	 * value. The instance type is determined by the type of the given field and
	 * the instance is created either by calling a string constructor or a
	 * static factory method on the field class.
	 * 
	 * @param field
	 *            field to create value for
	 * @param valueString
	 *            string representation of field value
	 * @return an object of the given field type representing the given value,
	 *         or {@code null} if the instance could not be created
	 */
	static Object makeValueInstance(Field field, String valueString) {

		// First try to create the value instance by invoking a string
		// constructor of the field class.

		Class<?> parameterType[] = new Class<?>[]{String.class};

		try {
			Constructor<?> constructor = ((Class<?>) field.getType())
					.getConstructor(parameterType);

			return constructor.newInstance(valueString);

		} catch (Exception e) {
			innerLog.log(Level.FINE,
					"Unable to use constuctor, using factory method instead", e);
		}

		// If there is no suitable constructor, try to create the instance by
		// invoking a static factory method.
		try {
			Method factoryMethod = ((Class<?>) field.getType()).getMethod(
					"valueOf", parameterType);
			Class<?> type = ((Class<?>) field.getType());

			if (type.isAssignableFrom(factoryMethod.getReturnType())) {
				return factoryMethod.invoke(null, valueString);
			}
		} catch (Exception e) {
			innerLog.log(Level.FINE,
					"Unable to use factory method instead, returning null", e);
		}

		// Could not create the instance, return null.
		return null;
	}

	/* ***********************************************************************
	 * Method-based property setter
	 * **********************************************************************
	 */

	/**
	 * Returns a method based {@link PropertySetter} bound to the given object
	 * and property name. When setting the property value, the returned
	 * {@link PropertySetter} will invoke a setter method annotated by the
	 * {@link Setter} annotation with matching name.
	 * 
	 * @param target
	 *            target object on which to set the property
	 * @param targetPropertyName
	 *            name of the property to set
	 * @return {@link PropertySetter} which allows to configure the property on
	 *         the given object, or {@code null} if the target object has no
	 *         setter method with matching annotation
	 */
	static PropertySetter makeMethodSetter(final Object target,
			final String targetPropertyName) {

		//
		// Find a setter method for the given property and create a
		// PropertySetter for the property.
		//
		// Specifically, enumerate all methods in the given object's class
		// hierarchy and find the first setter method annotated with the
		// @Setter annotation matching the given property field.
		//

		for (final Method method : new AllDeclaredMethodsIterable(
				target.getClass())) {

			Setter setterAnnotation = method.getAnnotation(Setter.class);

			if (setterAnnotation == null) {
				continue;
			}

			String propertyName = setterAnnotation.name();

			if (propertyName.length() == 0) {
				propertyName = getPropertyNameFromSetter(method);
			}

			if (targetPropertyName.equals(propertyName)) {
				//
				// Match found -- create the setter.
				//
				return new MethodPropertySetter(target, method,
						targetPropertyName);
			}
		}

		//
		// No match found.
		//
		return null;
	}

	/* ***********************************************************************
	 * PRIVATE METHODS
	 * **********************************************************************
	 */
	private static String getDefaultValue(Property property) {
        if (property.defaultValue().length() > 0) {
            return  property.defaultValue();
        }

        return null;
	}

	private static String getFieldName(Field field, Property property) {
        if (property.name().length() > 0) {
            return property.name();
        }

        return field.getName();
	}

	private static Object getFieldValue(Field field, Object target)
			throws IllegalArgumentException, IllegalAccessException {
		//
		// Make the field accessible before getting its value and
		// restore the previous accessibility state after that.
		//
		boolean originalAccessibility = field.isAccessible();
		field.setAccessible(true);
		Object value = field.get(target);
		field.setAccessible(originalAccessibility);
		return value;
	}

	private static void setFieldValue(Field field, Object target, Object value)
			throws IllegalArgumentException, IllegalAccessException {
		//
		// Make the field accessible before setting its value and
		// restore the previous accessibility state after that.
		//
		boolean originalAccesibility = field.isAccessible();

		field.setAccessible(true);
		field.set(target, value);
		field.setAccessible(originalAccesibility);
	}

	/* ***********************************************************************
	 * PropertySetter
	 * **********************************************************************
	 */

	/**
	 * 
	 * If the method name starts with "set", strip the prefix and lower case the
	 * first letter of the suffix.
	 */
	private static String getPropertyNameFromSetter(Method method) {
		String methodName = method.getName();
		if (methodName.startsWith("set")) {
			return methodName.substring(3, 4).toLowerCase()
					+ methodName.substring(4);
		} else {
			return methodName;
		}
	}

	/**
	 * Secures method to be accessible, invokes it and restores original
	 * accessibility afterwrads.
	 */
	private static void invokeMethod(final Object target, Method method,
			final String value) throws IllegalAccessException,
			InvocationTargetException {

		boolean originalAccesibility = method.isAccessible();
		method.setAccessible(true);
		method.invoke(target, value);
		method.setAccessible(originalAccesibility);
	}

	/* ***********************************************************************
	 * PropertySetter
	 * **********************************************************************
	 */
	/**
	 * Private interface encapsulating a method for setting a property value on
	 * an object. Instances of the {@link PropertySetter} interface are bound to
	 * a particular target object and property name at construction time -- only
	 * the value remains unbound and can be set.
	 */
	private interface PropertySetter {
		void setValue(String value);
	}

	/**
	 * Private implementation of PropertySetter for methods.
	 */
	private static class MethodPropertySetter implements PropertySetter {

		private Object target;
		private Method method;
		private String propertyName;

		public MethodPropertySetter(Object target, Method method,
				String propertyName) {
			this.target = target;
			this.method = method;
			this.propertyName = propertyName;
		}

		public void setValue(String value) {

			innerLog.log(Level.FINE, "setting method property %s to %s",
					new String[]{propertyName, value});

			Class<?> returnType = method.getReturnType();
			Class<?> paramType = method.getParameterTypes()[0];
			int numberParams = method.getParameterTypes().length;

			if (returnType != void.class || numberParams != 1
					|| paramType != String.class) {
				throw new ConfigurationException(
						"property %s: method %s() is not a setter",
						propertyName, method.getName());
			}

			try {
				invokeMethod(target, method, value);
			} catch (Exception e) {
				new ConfigurationException(e,
						"Unable to set property %s=%s using method %s()",
						propertyName, value, method.getName());
			}
		}
	}

	private static class FieldPropertySetter implements PropertySetter {

		private Object target;
		private Field field;
		private String propertyName;

		public FieldPropertySetter(Object target, Field field,
				String propertyName) {
			this.target = target;
			this.field = field;
			this.propertyName = propertyName;
		}

		public void setValue(String value) throws ConfigurationException {
			innerLog.log(Level.FINE, "setting field property %s to %s",
					new String[]{propertyName, value});
			configureFieldProperty(propertyName, target, value, field);
		}
	};

	/* ***********************************************************************
	 * ConfigurationException, ConfExc
	 * **********************************************************************
	 */

	/**
	 * Common exception for all configuration errors.
	 */
	public static class ConfigurationException extends RuntimeException {

		private static final long serialVersionUID = 1L;

		/**
		 * Formats {@link Exception#getMessage()} as
		 * {@link String#format(String, Object...)}
		 * 
		 * @param format
		 * @param args
		 */
		ConfigurationException(String format, Object... args) {
			super(String.format(format, args));
		}

		/**
		 * Wraps the given {@link Throwable} as a {@link ConfigurationException}
		 * along with an additional formatted message.
		 * 
		 */
		ConfigurationException(Throwable cause, String format, Object... args) {
			super(String.format(format, args), cause);
		}
	}

	/**
	 * Common exception for all configuration errors.
	 * 
	 * Deprecated use {@link ConfigurationException} instead
	 */
	@Deprecated
	public static class ConfExc extends ConfigurationException {

		private static final long serialVersionUID = 1L;

		ConfExc(String format, Object... args) {
			super(format, args);
		}

		ConfExc(Throwable cause, String format, Object... args) {
			super(cause, format, args);
		}
	}

	/* ***********************************************************************
	 * AllDeclaredFieldsIterable
	 * **********************************************************************
	 */

	/**
	 * Provides an iterable interface to fields declared in a class hierarchy.
	 * The provided iterator traverses the inheritance hierarchy from the given
	 * (leaf) class to the (root) base class and returns all fields declared in
	 * the classes along the inheritance path.
	 */
	static class AllDeclaredFieldsIterable implements Iterable<Field> {
		private Class<?> leaf;

		/**
		 * Creates an iterable for the given leaf class. If the leaf class is
		 * {@code null}, the iterable produces an empty iterator.
		 */
		AllDeclaredFieldsIterable(Class<?> leafClass) {
			leaf = leafClass;
		}

		@Override
		public Iterator<Field> iterator() {

			Collection<Field> allFields = new LinkedList<Field>();
			Class<?> clazz = leaf;
			while (clazz != null) {
				allFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
				clazz = clazz.getSuperclass();
			}

			return allFields.iterator();
		}
	}

	/* ***********************************************************************
	 * AllDeclaredMethodsIterable
	 * **********************************************************************
	 */

	/**
	 * Provides an iterable interface to methods declared in a class hierarchy.
	 * The provided iterator traverses the inheritance hierarchy from the given
	 * (leaf) class to the (root) base class and returns all methods declared in
	 * the classes along the inheritance path.
	 */
	static class AllDeclaredMethodsIterable implements Iterable<Method> {
		private Class<?> leaf;

		/**
		 * Creates an iterable for the given leaf class. If the leaf class is
		 * {@code null}, the iterable produces an empty iterator.
		 */
		AllDeclaredMethodsIterable(Class<?> leafClass) {
			leaf = leafClass;
		}

		@Override
		public Iterator<Method> iterator() {

			Collection<Method> allMethods = new LinkedList<Method>();
			Class<?> clazz = leaf;
			while (clazz != null) {
				allMethods.addAll(Arrays.asList(clazz.getDeclaredMethods()));
				clazz = clazz.getSuperclass();
			}

			return allMethods.iterator();
		}
	}

	/* *******************************************************************
	 * ArrayIterator
	 * ******************************************************************
	 */
	/**
	 * 
	 * Depracated use native Java solution as
	 * {@code Arrays.asList(clazz.getFields()).iterator()}
	 */
	@Deprecated
	static class ArrayIterator<E> implements Iterator<E> {
		private E[] array;
		private int length;
		private int nextPosition;

		/**
		 * 
		 * @see ArrayIterators
		 */
		@Deprecated
		ArrayIterator(E[] a) {
			array = a;
			length = a.length;
			nextPosition = 0;
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException(
					"cannot remove elements from array");
		}

		public boolean hasNext() {
			return nextPosition < length;
		}

		@Override
		public E next() {
			if (!hasNext())
				throw new NoSuchElementException();
			return array[nextPosition++];
		}
	}

}
